Миграции
========

> Note|Примечание: Миграции доступны с версии 1.1.6.

Как и исходный код, структура базы данных изменяется в процессе разработки
и поддержки приложения. К примеру, во время разработки может понадобиться
добавить новую таблицу или уже после размещения приложения на сервере добавить
индекс или столбец. При этом важно отслеживать изменения в структуре базы
данных (называемые **миграциями**) также, как мы делаем это для нашего исходного кода.
Если исходный код и база данных не соответствуют друг другу, скорее всего,
всё приложение не будет работать. Именно поэтому в Yii есть поддержка миграций,
позволяющая отслеживать изменения в базе данных, применять миграции или откатывать
уже применённые.

Ниже приведён пошаговый процесс использования миграций при разработке:

1. Иван создаёт новую миграцию (например, создающую новую таблицу).
2. Иван заливает её в систему контроля версий (SVN, GIT или другую).
3. Андрей обновляется из системы контроля версий и получает новую миграцию.
4. Андрей применяет миграцию к своей локальной базе данных.


В Yii управление миграциями производится через консольную команду `yiic migrate`,
которая поддерживает создание новых миграций, применение, откат и повторное
применение миграций, просмотр истории миграций и новых миграций.


> Note|Примечание: При работе с командой `migrate` рекомендуется использовать
> yiic приложения (то есть после `cd path/to/protected`), а не yiic из
> директории `framework`. Убедитесь, что директория `protected\migrations` существует и
> доступна для записи. Также проверьте настройки соединения с базой данных в `protected/config/console.php`.


Создание миграций
-----------------

Для создания новой миграции (например, создающей таблицу для новостей), мы должны
ввести в консоли:

~~~
yiic migrate create <name>
~~~

Обязательный параметр `name` должен содержать очень краткое описание миграции
(например, `create_news_table`). Как будет показано далее,
этот параметр используется как часть имени класса миграции, поэтому использовать
можно только буквы, цифры и знаки подчёркивания.

~~~
yiic migrate create create_news_table
~~~

Приведённая команда создаст в директории `protected/migrations` файл
`m101129_185401_create_news_table.php`, содержащий следующее:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
	}


    public function down()
    {
    	echo "m101129_185401_create_news_table does not support migration down.\n";
		return false;
    }

    /*
	// если требуется выполнить изменения внутри транзакции, используйте safeUp/safeDown
	// вместо up/down
	public function safeUp()
	{
	}

	public function safeDown()
	{
	}
	*/
}
~~~

Стоит отметить, что имя класса совпадает с именем файла и строится как
`m<timestamp>_<name>`, где `<timestamp>` — это время создания миграции в UTC
(в формате `yymmdd_hhmmss`), а `<name>` — то, что передано в параметре
`name` команды.

Метод `up()` должен содержать код, выполняющий миграцию, а метод `down()` может
содержать код, отменяющий сделанное в `up()`.

Иногда реализовать `down()` не получается. К примеру, если в `up()` из таблицы
удаляются данные, в `down()` вернуть их не получится. В этом случае миграция
называется необратимой, что означает невозможность возврата к предыдущему
состоянию базы данных. В приведённом выше коде метод `down()` возвращает `false`.
Это означает невозможность отката миграции.

> Info|Информация: Начиная с версии 1.1.7, если метод `up()` или метод `down()`
> возвращают `false`, все последующие миграции не будут применены. В 1.1.6 для
> этого было необходимо выкинуть исключение.

Рассмотрим в качестве примера миграцию, создающую таблицу с новостями.

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
		$this->createTable('tbl_news', array(
			'id' => 'pk',
			'title' => 'string NOT NULL',
			'content' => 'text',
		));
	}

	public function down()
	{
		$this->dropTable('tbl_news');
	}
}
~~~

Базовый класс [CDbMigration] предоставляет набор методов для работы с данными
и структурой базы данных. К примеру, при помощи [CDbMigration::createTable]
можно создать новую таблицу, а [CDbMigration::insert] добавит строку с данными.
Все эти методы используют подключение к базе данных, возвращаемое
[CDbMigration::getDbConnection()], что по умолчанию эквивалентно `Yii::app()->db`.

> Info|Информация: Как вы могли заметить, методы [CDbMigration] очень похожи на
методы [CDbCommand]. И это на самом деле так. Единственно отличие состоит в том, что
методы [CDbMigration] подсчитывают затрачиваемое на их выполнение время и
выводят сообщения о параметрах методов.


Транзакционные миграции
-----------------------

> Info|Информация: Данная возможность поддерживается, начиная с версии 1.1.7.

При применении сложных миграций для сохранения целостности данных
требуется либо выполнить все запросы, либо, если запрос не выполнился,
отменить предыдущие запросы. Для достижения этой цели можно использовать
транзакции.

Можно начать транзакцию явно и заключить в неё весь код, меняющий базу данных:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function up()
	{
		$transaction=$this->getDbConnection()->beginTransaction();
		try
		{
			$this->createTable('tbl_news', array(
				'id' => 'pk',
				'title' => 'string NOT NULL',
				'content' => 'text',
			));
			$transaction->commit();
		}
		catch(Exception $e)
		{
			echo "Exception: ".$e->getMessage()."\n";
			$transaction->rollback();
			return false;
		}
	}

	// …похожий код для down()
}
~~~

А можно сделать это проще, реализовав метод `safeUp()` вместо `up()` и `safeDown()`
вместо `down()`:

~~~
[php]
class m101129_185401_create_news_table extends CDbMigration
{
	public function safeUp()
	{
		$this->createTable('tbl_news', array(
			'id' => 'pk',
			'title' => 'string NOT NULL',
			'content' => 'text',
		));
	}

	public function safeDown()
	{
		$this->dropTable('tbl_news');
	}
}
~~~

Yii при применении миграции начнёт транзакцию, а затем выполнит код в
`safeUp()` или `safeDown()`. Если при этом возникнет какая-либо ошибка,
произойдёт откат транзакции, то есть база вернётся в начальное состояние.

> Note|Примечание: Не все СУБД полностью поддерживают транзакции и не для всех
> выражений. В том случае, если поддержки транзакций нет, реализовывать надо
> `up()` и `down()`. В случае использования MySQL или MariaDB некоторые выражения SQL могут вызвать
> неявное применение транзакции. Смотрите документацию [MySQL](http://dev.mysql.com/doc/refman/5.1/en/implicit-commit.html)
> и [MariaDB](https://mariadb.com/kb/en/sql-statements-that-cause-an-implicit-commit/).


Применение миграций
-------------------

Для того чтобы применить все новые миграции (то есть привести локальную БД в
актуальное состояние), следует запустить следующую команду:

~~~
yiic migrate
~~~

Команда покажет список всех новых миграций и, в случае утвердительного ответа,
по очереди запустит метод `up()` в каждом классе миграции в порядке их создания.

После применения миграции в таблицу `tbl_migration` будет внесена соответствующая
запись. Это позволяет узнать, какие миграции уже применены, а какие нет.
Если таблица `tbl_migration` не существует, она будет создана автоматически в
базе данных, указанной в компоненте `db` приложения.

Иногда требуется применить лишь одну или несколько новых миграций. Для этого можно
использовать следующую команду:

~~~
yiic migrate up 3
~~~

При этом применятся три новых миграции. Вместо трёх можно указать
любое количество применяемых миграций.

Также можно привести состояние базы данных к определённой версии:

~~~
yiic migrate to 101129_185401
~~~

В качестве параметра, указывающего версию, к которой надо привести базу данных,
используется часть имени файла, соответствующая времени создании миграции.
Если между последней применённой и указанной миграциями несколько миграций, то все
они будут применены. Если указанная миграция уже применялась, то будет произведён
откат всех миграций, применённых после неё (описано в следующем разделе).


Откат миграций
--------------

Для отката одной или нескольких последних применённых миграций можно
воспользоваться следующей командой:

~~~
yiic migrate down [step]
~~~

где необязательный параметр `step` задаёт количество миграций, которые надо откатить.
По умолчанию откатывается одна последняя применённая миграция.

Как было описано ранее, не все миграции можно откатить. При попытке отката таких
миграций будет выброшено исключение и процесс отката будет прерван.


Повторное применение миграций
-----------------------------

Повторное примение миграции производится путём последовательного отката и применения.
Осуществить это можно следующей командой:

~~~
yiic migrate redo [step]
~~~

где необязательный параметр `step` указывает количество миграций, которые необходимо
применить ещё раз. По умолчанию повторяется одна последняя миграция.


Просмотр информации о миграциях
-------------------------------

Кроме применения и отката миграций, инструмент миграций может отображать историю
миграций, а также новые, ещё не применённые миграции.

~~~
yiic migrate history [limit]
yiic migrate new [limit]
~~~

Здесь параметр `limit` указывает количество отображаемых миграций. Если `limit`
не указан, показываются все миграции.

Первая команда показывает уже применённые миграции, вторая — миграции, которые
ещё не были применены.


Изменение истории миграций
--------------------------

Иногда требуется изменить историю миграций так, чтобы текущая версия была
заменена на указанную без применения или отката миграций. Часто это требуется
при созданнии новой миграции. Для этого можно использовать следующую команду:

~~~
yiic migrate mark 101129_185401
~~~

Эта команда очень похожа на `yiic migrate to`, но она лишь изменяет таблицу истории
миграций до указанной версии без применения или отката самих миграций.


Настройка команды миграций
--------------------------

Есть несколько способов настроить команду миграций.

### Используя параметры командной строки

Команда миграций может быть настроена четыремя опциями:

* `interactive`: использовать ли интерактивный режим. По умолчанию true, то есть
при пременении миграции будет выводиться подтверждение. Если параметр выставлен
в false, то миграции можно применить в фоновом режиме.

* `migrationPath`: указывает директорию, в которой хранятся все файлы миграций.
Путь должен указываться в формате псевдонима, и соответствующая ему директория должна
существовать. Если параметр не указан, будет использована поддиректория
`migrations`, находящаяся внутри директории с приложением;

* `migrationTable`: указывает имя таблицы в базе данных, которая хранит историю
миграций. Значение по умолчанию равно `tbl_migration`. Структура таблицы следующая:
`version varchar(255) primary key, apply_time integer`;

* `connectionID`: указывает идентификатор компонента базы данных.
По умолчанию это 'db';

* `templateFile`: указывает путь к файлу, который используется как шаблон
для генерации классов миграций. Путь должен указываться как псевдоним
(то есть как `application.migrations.template`). Если путь не задан, будет
использоваться внутренний шаблон. В шаблоне токен `{ClassName}` будет заменён
именем класса миграции.

Для указания опций используется следующий формат:

~~~
yiic migrate up --option1=value1 --option2=value2 ...
~~~

К примеру, если необходимо мигрировать модуль `forum`, файлы миграций которого
расположены в директории модуля `migrations`, можно воспользоваться следующей командой:

~~~
yiic migrate up --migrationPath=ext.forum.migrations
~~~

Стоит отметить, что при передаче через командную строку флагов, таких как `interactive`, необходимо использовать
значения `1` или `0`:

~~~
yiic migrate --interactive=0
~~~

### Глобальная конфигурация команды

В то время как опции командной строки позволяют нам на лету конфигурировать команду
миграций, иногда требуется применить настройки раз и навсегда. К примеру, нам
может понадобиться использовать другую таблицу для хранения истории миграций или
использовать свой шаблон миграции. Это можно сделать, изменив настройки консольного
приложения следующим образом:

~~~
[php]
return array(
	......
	'commandMap'=>array(
		'migrate'=>array(
			'class'=>'system.cli.commands.MigrateCommand',
			'migrationPath'=>'application.migrations',
			'migrationTable'=>'tbl_migration',
			'connectionID'=>'db',
			'templateFile'=>'application.migrations.template',
		),
		......
	),
	......
);
~~~

Теперь при запуске команды `migrate` указанные выше настройки будут применены
без ввода каких-либо дополнительных параметров.